package stirling.software.SPDF.controller.api;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.WebResponseUtils;

@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
@RequiredArgsConstructor
@Slf4j
public class CropController {

    private final CustomPDFDocumentFactory pdfDocumentFactory;

    @PostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(
            summary = "Crops a PDF document",
            description =
                    "This operation takes an input PDF file and crops it according to the given"
                            + " coordinates. Input:PDF Output:PDF Type:SISO")
    public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm request) throws IOException {
        if (request.isRemoveDataOutsideCrop()) {
            return cropWithGhostscript(request);
        } else {
            return cropWithPDFBox(request);
        }
    }

    private ResponseEntity<byte[]> cropWithPDFBox(@ModelAttribute CropPdfForm request)
            throws IOException {
        PDDocument sourceDocument = pdfDocumentFactory.load(request);

        PDDocument newDocument =
                pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);

        int totalPages = sourceDocument.getNumberOfPages();

        LayerUtility layerUtility = new LayerUtility(newDocument);

        for (int i = 0; i < totalPages; i++) {
            PDPage sourcePage = sourceDocument.getPage(i);

            // Create a new page with the size of the source page
            PDPage newPage = new PDPage(sourcePage.getMediaBox());
            newDocument.addPage(newPage);
            PDPageContentStream contentStream =
                    new PDPageContentStream(newDocument, newPage, AppendMode.OVERWRITE, true, true);

            // Import the source page as a form XObject
            PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);

            contentStream.saveGraphicsState();

            // Define the crop area
            contentStream.addRect(
                    request.getX(), request.getY(), request.getWidth(), request.getHeight());
            contentStream.clip();

            // Draw the entire formXObject
            contentStream.drawForm(formXObject);

            contentStream.restoreGraphicsState();

            contentStream.close();

            // Now, set the new page's media box to the cropped size
            newPage.setMediaBox(
                    new PDRectangle(
                            request.getX(),
                            request.getY(),
                            request.getWidth(),
                            request.getHeight()));
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        newDocument.save(baos);
        newDocument.close();
        sourceDocument.close();

        byte[] pdfContent = baos.toByteArray();
        return WebResponseUtils.bytesToWebResponse(
                pdfContent,
                GeneralUtils.generateFilename(
                        request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
    }

    private ResponseEntity<byte[]> cropWithGhostscript(@ModelAttribute CropPdfForm request)
            throws IOException {
        PDDocument sourceDocument = pdfDocumentFactory.load(request);

        for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
            PDPage page = sourceDocument.getPage(i);
            PDRectangle cropBox =
                    new PDRectangle(
                            request.getX(),
                            request.getY(),
                            request.getWidth(),
                            request.getHeight());
            page.setCropBox(cropBox);
        }

        Path tempInputFile = Files.createTempFile("crop_input", ".pdf");
        Path tempOutputFile = Files.createTempFile("crop_output", ".pdf");

        try {
            sourceDocument.save(tempInputFile.toFile());
            sourceDocument.close();

            ProcessExecutor processExecutor =
                    ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT);
            List<String> command =
                    List.of(
                            "gs",
                            "-sDEVICE=pdfwrite",
                            "-dUseCropBox",
                            "-o",
                            tempOutputFile.toString(),
                            tempInputFile.toString());

            processExecutor.runCommandWithOutputHandling(command);

            byte[] pdfContent = Files.readAllBytes(tempOutputFile);

            return WebResponseUtils.bytesToWebResponse(
                    pdfContent,
                    request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
                            + "_cropped.pdf");

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Ghostscript processing was interrupted", e);
        } finally {
            try {
                Files.deleteIfExists(tempInputFile);
                Files.deleteIfExists(tempOutputFile);
            } catch (IOException e) {
                log.debug("Failed to delete temporary files", e);
            }
        }
    }
}
